home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / lib / rhythmbox / plugins / artdisplay / __init__.py next >
Encoding:
Python Source  |  2009-04-07  |  16.0 KB  |  456 lines

  1. # -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
  2. #
  3. # Copyright (C) 2006 - James Livingston
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2, or (at your option)
  8. # any later version.
  9. #
  10. # The Rhythmbox authors hereby grant permission for non-GPL compatible
  11. # GStreamer plugins to be used and distributed together with GStreamer
  12. # and Rhythmbox. This permission is above and beyond the permissions granted
  13. # by the GPL license by which Rhythmbox is covered. If you modify this code
  14. # you may extend this exception to your version of the code, but you are not
  15. # obligated to do so. If you do not wish to do so, delete this exception
  16. # statement from your version.
  17. #
  18. # This program is distributed in the hope that it will be useful,
  19. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  21. # GNU General Public License for more details.
  22. #
  23. # You should have received a copy of the GNU General Public License
  24. # along with this program; if not, write to the Free Software
  25. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
  26.  
  27. import rhythmdb, rb
  28. import gtk, gobject
  29. from warnings import warn
  30.  
  31. from CoverArtDatabase import CoverArtDatabase
  32.  
  33.  
  34. FADE_STEPS = 10
  35. FADE_TOTAL_TIME = 1000
  36. ART_MISSING_ICON = 'rhythmbox-missing-artwork'
  37. WORKING_DELAY = 500
  38. THROBBER_RATE = 10
  39. THROBBER = 'gnome-spinner'
  40. ASPECT_RATIO_MIN = 0.9
  41. ASPECT_RATIO_MAX = 1.1
  42.  
  43. def merge_pixbufs (old_pb, new_pb, reserve_pb, step, width, height, mode=gtk.gdk.INTERP_BILINEAR):
  44.     if width <= 1 and height <= 1:
  45.         return None
  46.     if old_pb is None:
  47.         if new_pb is None:
  48.             return reserve_pb
  49.         else:
  50.             return new_pb.scale_simple (width, height, mode)
  51.     elif step == 0.0:
  52.         return old_pb.scale_simple (width, height, mode)
  53.     elif new_pb is None:
  54.         if reserve_pb is None:
  55.             return None
  56.         new_pb = reserve_pb
  57.     sw, sh = (float (width)) / new_pb.props.width, (float (height)) / new_pb.props.height
  58.     alpha = int (step * 255)
  59.     ret = old_pb.scale_simple (width, height, mode)
  60.     new_pb.composite (ret, 0, 0, width, height, 0, 0, sw, sh, mode, alpha)
  61.     return ret
  62.  
  63. def merge_with_background (pixbuf, bgcolor, pad_if_not_near_square):
  64.     if pixbuf is None:
  65.         return pixbuf
  66.     has_alpha = pixbuf.get_has_alpha ()
  67.     width, height = pixbuf.props.width, pixbuf.props.height
  68.     if pad_if_not_near_square and (height < width * ASPECT_RATIO_MIN or
  69.                        height > width * ASPECT_RATIO_MAX):
  70.         rw, rh = max (width, height), max (width, height)
  71.         left, top = (rw - width) // 2, (rh - height) // 2
  72.     else:
  73.         if not has_alpha:
  74.             return pixbuf
  75.         rw, rh, left, top = width, height, 0, 0
  76.     ret = gtk.gdk.Pixbuf (gtk.gdk.COLORSPACE_RGB, False, 8, rw, rh)
  77.     ret.fill (((bgcolor.red & 0xff00) << 16) | ((bgcolor.green & 0xff00) << 8) | (bgcolor.blue & 0xff00) | 0xff)
  78.     if has_alpha:
  79.         pixbuf.composite (ret, left, top, width, height, left, top, 1.0, 1.0, gtk.gdk.INTERP_NEAREST, 255)
  80.     else:
  81.         pixbuf.copy_area (0, 0, width, height, ret, left, top)
  82.     return ret
  83.  
  84. class FadingImage (gtk.Misc):
  85.     __gsignals__ = { 'size-allocate': 'override' }
  86.     def __init__ (self, missing_image):
  87.         gobject.GObject.__init__ (self)
  88.         self.sc_id = self.connect('screen-changed', self.screen_changed)
  89.         self.ex_id = self.connect ('expose-event', self.expose)
  90.         self.sr_id = self.connect ('size-request', self.size_request)
  91.         self.resize_id, self.fade_id, self.anim_id = 0, 0, 0
  92.         self.missing_image, self.size = missing_image, 100
  93.         self.screen_changed (self, None)
  94.         self.old_pixbuf, self.new_pixbuf = None, None
  95.         self.merged_pixbuf, self.missing_pixbuf = None, None
  96.         self.fade_step = 0.0
  97.         self.anim, self.anim_frames, self.anim_size = None, None, 0
  98.  
  99.     def disconnect_handlers (self):
  100.         for id in self.sc_id, self.ex_id, self.sr_id:
  101.             self.disconnect(id)
  102.         self.icon_theme.disconnect(self.tc_id)
  103.         for id in self.resize_id, self.fade_id, self.anim_id:
  104.             if id != 0:
  105.                 gobject.source_remove (id)
  106.  
  107.     def screen_changed (self, widget, old_screen):
  108.         if old_screen:
  109.             self.icon_theme.disconnect (self.tc_id)
  110.         self.icon_theme = gtk.icon_theme_get_for_screen (self.get_screen ())
  111.         self.tc_id = self.icon_theme.connect ('changed', self.theme_changed)
  112.         self.theme_changed (self.icon_theme)
  113.  
  114.     def reload_anim_frames (self):
  115.         icon_info = self.icon_theme.lookup_icon (THROBBER, -1, 0)
  116.         size = icon_info.get_base_size ()
  117.         icon = gtk.gdk.pixbuf_new_from_file (icon_info.get_filename ())
  118.         self.anim_frames = [ # along, then down
  119.                 icon.subpixbuf (x * size, y * size, size, size)
  120.                 for y in range (int (icon.props.height / size))
  121.                 for x in range (int (icon.props.width / size))]
  122.         self.anim_size = size
  123.  
  124.     def theme_changed (self, icon_theme):
  125.         try:
  126.             self.reload_anim_frames ()
  127.         except Exception, e:
  128.             warn ("Throbber animation not loaded: %s" % e, Warning)
  129.         self.reload_util_pixbufs ()
  130.  
  131.     def reload_util_pixbufs (self):
  132.         if self.size <= 1:
  133.             return
  134.         try:
  135.             missing_pixbuf = self.icon_theme.load_icon (ART_MISSING_ICON, self.size, 0)
  136.         except:
  137.             try:
  138.                 missing_pixbuf = gtk.gdk.pixbuf_new_from_file_at_size (self.missing_image, self.size, self.size)
  139.             except Exception, e:
  140.                 warn ("Missing artwork icon not found: %s" % e, Warning)
  141.                 return
  142.         self.missing_pixbuf = merge_with_background (missing_pixbuf, self.style.bg[gtk.STATE_NORMAL], False)
  143.  
  144.     def do_size_allocate (self, allocation):
  145.         self.allocation = allocation
  146.         if self.resize_id == 0:
  147.             self.resize_id = gobject.idle_add (self.after_resize)
  148.         if self.size != allocation.width:
  149.             self.size = allocation.width
  150.             self.queue_resize ()
  151.         elif self.window is not None:
  152.             self.window.move_resize (allocation.x, allocation.y, allocation.width, allocation.height)
  153.             self.queue_draw ()
  154.             self.window.process_updates (True)
  155.  
  156.     def after_resize (self):
  157.         self.reload_util_pixbufs ()
  158.         self.merged_pixbuf = None
  159.         self.queue_draw ()
  160.         return False
  161.  
  162.     def size_request (self, widget, requisition):
  163.         requisition.width, requisition.height = -1, self.size
  164.  
  165.     def expose (self, widget, event):
  166.         if not self.ensure_merged_pixbuf ():
  167.             return False
  168.         if self.merged_pixbuf.props.width != self.size:
  169.             draw_pb = self.merged_pixbuf.scale_simple (self.size, self.size, gtk.gdk.INTERP_NEAREST)
  170.         else:
  171.             draw_pb = self.merged_pixbuf
  172.         x, y, w, h = event.area
  173.         event.window.draw_pixbuf (None, draw_pb, x, y, x, y, min (w, self.size - x), min (h, self.size - y))
  174.         if self.anim:
  175.             x, y, w, h = self.anim_rect ()
  176.             event.window.draw_pixbuf (None, self.anim, max (0, -x), max (0, -y), max (0, x), max (0, y), w, h)
  177.         return False
  178.  
  179.     def anim_rect (self):
  180.         return gtk.gdk.Rectangle (
  181.                 (self.allocation.width - self.anim_size) / 2,
  182.                 (self.allocation.height - self.anim_size) / 2,
  183.                 min (self.anim_size, self.allocation.width),
  184.                 min (self.anim_size, self.allocation.height))
  185.  
  186.     def ensure_merged_pixbuf (self):
  187.         if self.merged_pixbuf is None:
  188.             self.merged_pixbuf = merge_pixbufs (self.old_pixbuf, self.new_pixbuf, self.missing_pixbuf, self.fade_step, self.allocation.width, self.allocation.height)
  189.         return self.merged_pixbuf
  190.  
  191.     def render_overlay (self):
  192.         ret = self.ensure_merged_pixbuf ()
  193.         if ret and self.anim:
  194.             if ret is self.missing_pixbuf: ret = ret.copy ()
  195.             x, y, w, h = self.anim_rect ()
  196.             self.anim.composite (ret, max (x, 0), max (y, 0), w, h, x, y, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
  197.         return ret
  198.  
  199.     def fade_art (self, first_time):
  200.         self.fade_step += 1.0 / FADE_STEPS
  201.         if self.fade_step > 0.999:
  202.             self.old_pixbuf = None
  203.             self.fade_id = 0
  204.         self.merged_pixbuf = None
  205.         if first_time:
  206.             self.fade_id = gobject.timeout_add ((FADE_TOTAL_TIME / FADE_STEPS), self.fade_art, False)
  207.             return False
  208.         self.queue_resize ()
  209.         return (self.fade_step <= 0.999)
  210.  
  211.     def animation_advance (self, counter, first_time):
  212.         self.anim = self.anim_frames[counter[0]]
  213.         counter[0] = (counter[0] + 1) % len(self.anim_frames)
  214.         x, y, w, h = self.anim_rect ()
  215.         self.queue_draw_area (max (x, 0), max (y, 0), w, h)
  216.         if first_time:
  217.             self.anim_id = gobject.timeout_add (int (1000 / THROBBER_RATE), self.animation_advance, counter, False)
  218.             return False
  219.         return True
  220.  
  221.     def set_current_art (self, pixbuf, working):
  222.         if self.props.visible and self.parent.allocation.width > 1:
  223.             self.old_pixbuf = self.render_overlay ()
  224.         else:
  225.             self.old_pixbuf = None    # don't fade
  226.         self.new_pixbuf = merge_with_background (pixbuf, self.style.bg[gtk.STATE_NORMAL], True)
  227.         self.merged_pixbuf = None
  228.         self.fade_step = 0.0
  229.         self.anim = None
  230.         if self.fade_id != 0:
  231.             gobject.source_remove (self.fade_id)
  232.             self.fade_id = 0
  233.         if self.old_pixbuf is not None:
  234.             self.fade_id = gobject.timeout_add (working and WORKING_DELAY or (FADE_TOTAL_TIME / FADE_STEPS), self.fade_art, working)
  235.         if working and self.anim_id == 0 and self.anim_frames:
  236.             self.anim_id = gobject.timeout_add (WORKING_DELAY, self.animation_advance, [0], True)
  237.         if not working and self.anim_id != 0:
  238.             gobject.source_remove (self.anim_id)
  239.             self.anim_id = 0
  240.         self.queue_resize ()
  241. gobject.type_register (FadingImage)
  242.  
  243.  
  244. class ArtDisplayWidget (FadingImage):
  245.     __gsignals__ = {
  246.             'pixbuf-dropped' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (rhythmdb.Entry, gtk.gdk.Pixbuf)),
  247.             'uri-dropped' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (rhythmdb.Entry, gobject.TYPE_STRING))
  248.             }
  249.  
  250.     def __init__ (self, missing_image):
  251.         super (ArtDisplayWidget, self).__init__ (missing_image)
  252.         self.set_padding (0, 5)
  253.         self.ddg_id = self.connect ('drag-data-get', self.drag_data_get)
  254.         self.ddr_id = self.connect ('drag-data-received', self.drag_data_received)
  255.         self.current_entry, self.working = None, False
  256.         self.current_pixbuf, self.current_uri = None, None
  257.  
  258.     def disconnect_handlers (self):
  259.          super (ArtDisplayWidget, self).disconnect_handlers ()
  260.         self.disconnect (self.ddg_id)
  261.         self.disconnect (self.ddr_id)
  262.     
  263.     def update_dnd_targets (self):
  264.         targets = None
  265.         if self.current_entry:
  266.             targets = gtk.target_list_add_image_targets (targets)
  267.             targets = gtk.target_list_add_uri_targets (targets)
  268.             targets = gtk.target_list_add_text_targets (targets)
  269.         if targets:
  270.             self.drag_dest_set (gtk.DEST_DEFAULT_ALL, targets, gtk.gdk.ACTION_COPY)
  271.         else:
  272.             self.drag_dest_unset ()
  273.  
  274.         targets = None
  275.         if self.current_pixbuf:
  276.             targets = gtk.target_list_add_image_targets (targets, writable=True)
  277.         if self.current_uri:
  278.             targets = gtk.target_list_add_uri_targets (targets)
  279.         if targets:
  280.             self.drag_source_set (gtk.gdk.BUTTON1_MASK, targets, gtk.gdk.ACTION_COPY)
  281.         else:
  282.             self.drag_source_unset ()
  283.  
  284.     def update_tooltips (self, working):
  285.         if not self.current_entry:
  286.             self.set_tooltip_text (None)
  287.         elif working:
  288.             self.set_tooltip_text (_("Searching... drop artwork here"))
  289.         else:
  290.             self.set_tooltip_text (_("Drop artwork here"))
  291.  
  292.     def set (self, entry, pixbuf, uri, working):
  293.         self.current_entry = entry
  294.         self.current_pixbuf = pixbuf
  295.         self.current_uri = uri
  296.         self.set_current_art (pixbuf, working)
  297.         self.update_dnd_targets ()
  298.         self.update_tooltips (working)
  299.  
  300.     def drag_data_get (self, widget, drag_context, selection_data, info, timestamp):
  301.         if self.current_pixbuf:
  302.             selection_data.set_pixbuf (self.current_pixbuf)
  303.         if self.current_uri:
  304.             selection_data.set_uris ([self.current_uri])
  305.  
  306.     def drag_data_received (self, widget, drag_context, x, y, selection_data, info, timestamp):
  307.         entry = self.current_entry
  308.         pixbuf = selection_data.get_pixbuf ()
  309.         uris = selection_data.get_uris ()
  310.         text = selection_data.get_text ()
  311.         if pixbuf:
  312.             self.emit ('pixbuf-dropped', entry, pixbuf)
  313.         elif uris:
  314.             self.emit ('uri-dropped', entry, uris[0])
  315.         elif text:
  316.             self.emit ('uri-dropped', entry, text)
  317. gobject.type_register (ArtDisplayWidget)
  318.  
  319.  
  320. class ArtDisplayPlugin (rb.Plugin):
  321.     def __init__ (self):
  322.         rb.Plugin.__init__ (self)
  323.  
  324.     def activate (self, shell):
  325.         self.shell = shell
  326.         sp = shell.get_player ()
  327.         self.player_cb_ids = (
  328.             sp.connect ('playing-song-changed', self.playing_entry_changed),
  329.             sp.connect ('playing-changed', self.playing_changed)
  330.         )
  331.         db = shell.get_property ("db")
  332.         self.db_cb_ids = (
  333.             db.connect_after ('entry-extra-metadata-request::rb:coverArt', self.cover_art_request),
  334.             db.connect_after ('entry-extra-metadata-notify::rb:coverArt', self.cover_art_notify),
  335.             db.connect_after ('entry-extra-metadata-request::rb:coverArt-uri', self.cover_art_uri_request),
  336.             db.connect_after ('entry-extra-metadata-notify::rb:coverArt-uri', self.cover_art_uri_notify),
  337.             db.connect_after ('entry-extra-metadata-gather', self.cover_art_uri_gather),
  338.         )
  339.         self.art_widget = ArtDisplayWidget (self.find_file (ART_MISSING_ICON + ".svg"))
  340.         self.art_widget.connect ('pixbuf-dropped', self.on_set_pixbuf)
  341.         self.art_widget.connect ('uri-dropped', self.on_set_uri)
  342.         self.art_container = gtk.VBox ()
  343.         self.art_container.pack_start (self.art_widget, padding=6)
  344.         shell.add_widget (self.art_container, rb.SHELL_UI_LOCATION_SIDEBAR)
  345.         self.art_db = CoverArtDatabase ()
  346.         self.current_entry, self.current_pixbuf = None, None
  347.         self.playing_entry_changed (sp, sp.get_playing_entry ())
  348.  
  349.     def deactivate (self, shell):
  350.         self.shell = None
  351.  
  352.         sp = shell.get_player ()
  353.         for id in self.player_cb_ids:
  354.             sp.disconnect (id)
  355.         self.player_cb_ids = ()
  356.  
  357.         db = shell.get_property ("db")
  358.         for id in self.db_cb_ids:
  359.             db.disconnect (id)
  360.         self.db_cb_ids = ()
  361.  
  362.         shell.remove_widget (self.art_container, rb.SHELL_UI_LOCATION_SIDEBAR)
  363.         self.art_widget.disconnect_handlers ()
  364.         self.art_widget = None
  365.         self.art_db = None
  366.  
  367.     def playing_changed (self, sp, playing):
  368.         self.set_entry(sp.get_playing_entry ())
  369.  
  370.     def playing_entry_changed (self, sp, entry):
  371.         self.set_entry(entry)
  372.  
  373.     def set_entry (self, entry):
  374.         if entry == self.current_entry:
  375.             return
  376.         db = self.shell.get_property ("db")
  377.  
  378.         self.art_widget.set (entry, None, None, True)
  379.         self.art_container.show_all ()
  380.         # Intitates search in the database (which checks art cache, internet etc.)
  381.         self.current_entry = entry
  382.         self.current_pixbuf = None
  383.         self.art_db.get_pixbuf(db, entry, self.on_get_pixbuf_completed)
  384.  
  385.     def on_get_pixbuf_completed(self, entry, pixbuf, uri):
  386.         # Set the pixbuf for the entry returned from the art db
  387.         if entry != self.current_entry:
  388.             return
  389.         self.current_pixbuf = pixbuf
  390.         self.art_widget.set (entry, pixbuf, uri, False)
  391.         if pixbuf:
  392.             db = self.shell.get_property ("db")
  393.             # This might be from a playing-changed signal,
  394.             # in which case consumers won't be ready yet.
  395.             def idle_emit_art():
  396.                 db.emit_entry_extra_metadata_notify (entry, "rb:coverArt", pixbuf)
  397.                 return False
  398.             gobject.idle_add(idle_emit_art)
  399.  
  400.     def cover_art_request (self, db, entry):
  401.         if entry == self.current_entry:
  402.             return self.current_pixbuf
  403.  
  404.     def cover_art_notify (self, db, entry, field, metadata):
  405.         if entry != self.current_entry:
  406.             return
  407.         if not isinstance (metadata, gtk.gdk.Pixbuf):
  408.             return
  409.         self.art_db.cancel_get_pixbuf (entry)
  410.         if self.current_pixbuf == metadata:
  411.             return
  412.         self.art_widget.set (entry, metadata, None, False)
  413.  
  414.     def cover_art_uri_notify (self, db, entry, field, metadata):
  415.         if entry != self.current_entry:
  416.             return
  417.  
  418.         if not metadata:
  419.             print "got no-cover-art notification"
  420.             self.art_widget.set (entry, None, None, False)
  421.             db.emit_entry_extra_metadata_notify (entry, "rb:coverArt", None)
  422.             return
  423.  
  424.         uri = str (metadata)
  425.         def loader_cb (data):
  426.             if data and len (data) >= 1000:
  427.                 pbl = gtk.gdk.PixbufLoader ()
  428.                 try:
  429.                     if pbl.write (data) and pbl.close ():
  430.                         pixbuf = pbl.get_pixbuf ()
  431.                         if pixbuf:
  432.                             self.art_db.cancel_get_pixbuf (entry)
  433.                             self.on_get_pixbuf_completed (entry, pixbuf, uri)
  434.                 except GError:
  435.                     pass
  436.  
  437.         print "got cover art URI notification: %s" % (uri)
  438.         l = rb.Loader()
  439.         l.get_url (uri, loader_cb)
  440.  
  441.     def cover_art_uri_request (self, db, entry):
  442.         if entry == self.current_entry:
  443.             return self.art_widget.current_uri
  444.  
  445.     def cover_art_uri_gather (self, db, entry, metadata):
  446.         if entry == self.current_entry and self.art_widget.current_uri:
  447.             metadata ['rb:coverArt-uri'] = self.art_widget.current_uri
  448.  
  449.     def on_set_pixbuf (self, widget, entry, pixbuf):
  450.         db = self.shell.get_property ("db")
  451.         self.art_db.set_pixbuf (db, entry, pixbuf, self.on_get_pixbuf_completed)
  452.  
  453.     def on_set_uri (self, widget, entry, uri):
  454.         db = self.shell.get_property ("db")
  455.         self.art_db.set_pixbuf_from_uri (db, entry, uri, self.on_get_pixbuf_completed)
  456.